package org.hzero.gateway.helper.resolver;

import org.hzero.gateway.helper.entity.CommonRoute;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.route.CompositeRouteDefinitionLocator;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.EventListener;
import org.springframework.util.CollectionUtils;

import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Gateway属性解析器,主要就是要获取到所有服务的路由信息
 * @author XCXCXCXCX
 * @date 2019/9/3
 * @project hzero-gateway-helper
 */
public class GatewayPropertiesResolver implements PropertiesResolver<GatewayProperties> {

    private static final String LB_SCHEME = "lb";
    private static final String LB_PREFIX = "lb://";

    private final RouteDefinitionLocator locator;

    private List<RouteDefinition> cache = new ArrayList<>();

    public GatewayPropertiesResolver(Object locator) {
        this.locator = (CompositeRouteDefinitionLocator) locator;
        // Flux
        // 响应式编程宣言 https://www.reactivemanifesto.org/zh-CN
        // 对于响应式框架，是基于响应式宣言的理念所产生的编程方式。响应式宣言分为4大理念。
        // •灵敏的：就是可以快速响应的，只要有任何可能，系统都应该能够尽可能快地做出响应。
        // •可恢复的：系统在运行中可能出现问题，但是能够有很强大的容错机制和修复机制保持响应性。
        // •可伸缩的：在任何负载下，响应式编程都可以根据自身压力变化，请求少时，通过减少资源释放服务器的压力，负载大时能够通过扩展算法和软硬件的方式扩展服务能力，以经济实惠的方式实现可伸缩性。
        // •消息驱动的：响应式编程存在异步消息机制，事件之间的协作是通过消息进行连接的。

        // 响应式框架中，将集中接收请求并分发出去的模块拆分成一个selector和多个客户端和响应器
        // 客户端会向服务注册器注册事件，但是并不会给服务器发送请求。当客户端发生一些已经注册的事件时，就会触发服务器的响应。
        // 当触发服务器响应时，服务器存在一个 selector 线程，这个线程只是复杂轮询客户端发送过来的事件，并不处理请求，
        // 当它接收到有客户端事件时，就会找到对应的请求处理器，然后启用另外一条线程运行处理器。

        // 在 spring5 中对于响应式编程而言分为Router Functions、Spring WebFlux和HTTP/Reactive Streams共3层。
        // •Router Functions：是一个路由分发层，也就是它会根据请求的事件，决定采用什么类的什么方法处理客户端发送过来的事件请求。显然，在Reactor模式中，它就是Selector的作用。
        // •Spring-webflux：是一种控制层，类似Spring MVC框架的层级，它主要处理业务逻辑前进行的封装和控制数据流返回格式等。
        //• HTTP/Reactive Streams：是将结果转换为数据流的过程。


        // 与Spring MVC使用DispatcherServlet不同的是Spring WebFlux使用的是WebHandler。
        // webHandler有几个核心关系类
        // - DispatcherHandler 分发处理器
        // - WebHandlerDecorator 装饰者
        //   - ExceptionHandlingWebHandler
        //   - FilteringWebHandler
        //   - HttpWebHandlerAdapter
        // - ResourceWebHandler 资源处理器
        this.locator.getRouteDefinitions()
                .filter(this::checkRouteDefinition)
                .subscribe(cache::add);
    }

    /**
     * 主要功能就是解析 默认RouteDefinitionLocator提供的路由和配置属性里提供的路由,将他们放入缓存中
     * @description
     * @since 1.0
     * @version 1.0
     * @author tianhao.luo@hand-china.com
     * @date 2022/1/10 10:03 下午
     * @param properties 配置属性
     * @return 路由缓存映射
     */
    @Override
    public Map<String, CommonRoute> resolveRoutes(GatewayProperties properties) {
        List<RouteDefinition> routeDefinitions = new ArrayList<>();
        routeDefinitions.addAll(cache);
        routeDefinitions.addAll(properties.getRoutes());
        if (!CollectionUtils.isEmpty(cache)) {
            Map<String, CommonRoute> returnVal = new HashMap<>(32);
            routeDefinitions
                    .forEach(routeDefinition -> {
                        CommonRoute route = new CommonRoute();
                        String id = routeDefinition.getId();
                        URI uri = routeDefinition.getUri();
                        List<PredicateDefinition> predicateDefinitions = routeDefinition.getPredicates();
                        List<FilterDefinition> filterDefinitions = routeDefinition.getFilters();
                        route.setId(id);
                        //从uri中解析serviceId，如果不是lb://{serviceId}则无法获取serviceId
                        //此时采用路由id作为serviceId（由于权限key需要有serviceId）
                        route.setServiceId(resolveServiceId(uri, id));
                        route.setUrl(resolveString(predicateDefinitions, "Url"));
                        route.setPath(resolveString(predicateDefinitions, "Path"));
                        route.setStripPrefix(resolveBoolean(filterDefinitions, "StripPrefix", true));
                        //route.setSensitiveHeaders(resolveSensitiveHeaders(filterDefinitions));
                        //route.setCustomSensitiveHeaders(resolveBoolean(filterDefinitions, "CustomSensitiveHeaders", false));
                        //route.setLocation();
                        //route.setRetryable();
                        returnVal.put(id, route);
                    });
            return returnVal;
        }
        return Collections.emptyMap();
    }

    private String resolveString(List<PredicateDefinition> predicateDefinitions, String name) {
        if (!CollectionUtils.isEmpty(predicateDefinitions)) {
            return predicateDefinitions.stream()
                    .filter(predicateDefinition -> name.equals(predicateDefinition.getName()) && predicateDefinition.getArgs() != null)
                    .findFirst()
                    .map(predicateDefinition -> predicateDefinition.getArgs().entrySet()
                                .stream().findFirst().map(Map.Entry::getValue).get()
                    ).orElseGet(() -> null);
        }
        return null;
    }

    private boolean checkRouteDefinition(RouteDefinition routeDefinition){
        URI uri = routeDefinition.getUri();
        if(uri == null){
            return false;
        }
        return true;
    }

    private Set<String> resolveSensitiveHeaders(List<FilterDefinition> filterDefinitions) {
        if (!CollectionUtils.isEmpty(filterDefinitions)) {
            return filterDefinitions.stream()
                    .filter(filterDefinition -> "SensitiveHeaders".equals(filterDefinition.getName()) && filterDefinition.getArgs() != null)
                    .findFirst()
                    .map(filterDefinition -> {
                        Map.Entry<String, String> first = filterDefinition.getArgs().entrySet()
                                .stream().findFirst().get();
                        Set<String> set = new HashSet<>();
                        String[] parts = first.getValue() == null ? new String[]{} : first.getValue().split(",");
                        for (String part : parts){
                            if(!"".equals(part)){
                                set.add(part);
                            }
                        }
                        return set;
                    }).orElseGet(Collections::emptySet);
        }
        return Collections.emptySet();
    }

    private boolean resolveBoolean(List<FilterDefinition> filterDefinitions, String name, boolean defaultValue) {
        if (!CollectionUtils.isEmpty(filterDefinitions)) {
            return filterDefinitions.stream()
                    .filter(filterDefinition -> name.equals(filterDefinition.getName()) && filterDefinition.getArgs() != null)
                    .findFirst()
                    .map(filterDefinition -> {
                        Map.Entry<String, String> first = filterDefinition.getArgs().entrySet()
                                .stream().findFirst().get();
                        return first != null && ("1".equals(first.getValue()) || Boolean.parseBoolean(first.getValue()));
                    }).orElseGet(() -> false);
        }
        return defaultValue;
    }

    private static String resolveServiceId(URI uri, String defaultValue) {
        if (LB_SCHEME.equals(uri.getScheme())) {
            return uri.getAuthority();
        }
        return defaultValue;
    }

    @EventListener(RefreshRoutesEvent.class)
    public void onApplicationEvent(ApplicationEvent event) {
        cache = new ArrayList<>();
        locator.getRouteDefinitions().subscribe(cache::add);
    }
}
